package vsvm.data.filter.ranking;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;

import org.apache.commons.math.stat.descriptive.moment.Variance;

public class LaplacianScore extends AbstractUnsupervisedRanking {

	private static final long serialVersionUID = 100L;
	private int knn;
	private double t;
	private double eps;
	private String option;
	
	public static final String epsilon = "epsilon";
	public static final String nearestNeighbor = "nearestNeighbor";
	public static final String labelInfo = "labelInfo";
	
	public LaplacianScore(double eps, int knn, double m, String option){
		
		this.knn = knn;
		if(m > 1e-8)
			t = 1/m;
		else 
			t = 0;
		this.eps = eps;
		this.option = option;
	}
	
	public LaplacianScore()
	{
		knn = 4;
		t = 0;
		option = LaplacianScore.nearestNeighbor;
	}
	
	public double[] score() 
	{
		int numAttr = dataModel.getAttributeCount();
		int numVec  = dataModel.getVectorCount();
		
		double[] variance = new double[numAttr];
		Variance var = new Variance();
		
		for(int i = 0; i < numAttr; i++)
		{
			if(i != dataModel.getClassAttributeIndex())
			{
				variance[i] = var.evaluate(allFeatureValues(i));
			}
		}
		
		double[][] sim = similarity(numAttr, numVec);

/*		for(int i = 0; i < sim.length; i++)
		{
			for(int j = 0; j < sim.length; j++)
			{
				System.out.print((int)sim[i][j] + " ");
			}	
			System.out.println();
		}*/
		
		double[] numerator = new double[numAttr];
		for(int i = 0; i < numAttr; i++)
		{
			if(i != dataModel.getClassAttributeIndex())
			{
				double[] values = allFeatureValues(i);
				for(int j = 0; j < numVec; j++)
				{
					for(int t = 0; t < numVec; t++)
					{
						numerator[i] += Math.pow((values[j] - values[t]), 2) * sim[j][t];						
					}											
				}
			}			
		}
		
		double maxRatio = 0;
		
		for(int i = 0; i < numAttr; i ++)
		{
			if(i != dataModel.getClassAttributeIndex() && variance[i] > 1e-8)
			{
				 if(maxRatio < numerator[i]/variance[i])
				 {
					 maxRatio = numerator[i]/variance[i];
				 }
			} 
		}
		
		double[] scores = new double[numAttr];
		
		for(int i = 0; i < numAttr; i++)
		{
			if(i != dataModel.getClassAttributeIndex())
			{
				if(variance[i] > 1e-8)
				{
					scores[i] = numerator[i]/variance[i];
				} 
				else
				{
					scores[i] = maxRatio;
				}
			} 
		}
		
		for(int i = 0; i < scores.length; i++)
		{
			if(i != dataModel.getClassAttributeIndex())
			{
				scores[i] = maxRatio - scores[i];	
			}
			else
			{
				scores[i] = maxRatio;
			}
		}
		
		return scores;
	}
	
	private double getSimilarityCoef(double dist)
	{
		return Math.exp(t * dist * dist);
	}
	
	private double[][] similarity(int attr, int vec)
	{	
		double[][] distances = distance(attr, vec);
		
		if(option.equals(epsilon))
		{
			return simEps(vec, distances);
		}
		else if(option.equals(nearestNeighbor))
		{
			return simKnn(vec, distances);
		}
		return simLabel(vec, distances);
	}
	
	private double[][] simLabel(int vec, double[][] distance)
	{
		double[][] sim = new double[vec][vec];
		HashSet<Double> set = new HashSet<Double>();
		
		for(int i = 0; i < vec; i++)
		{
			double mainLabel = dataModel.getClass(i);
			if(!set.contains(mainLabel))
			{
				set.add(mainLabel);
				
				for(int j = 0; j < vec; j++)
				{		
					if(i != j)
					{
						double label = dataModel.getClass(j);
						if(label == mainLabel)
						{
							double coef = getSimilarityCoef(distance[i][j]);
							sim[i][j] = coef;
							sim[j][i] = coef;	
						}	
					}
				}		
			}		
		}
		
		return sim;
	}
	
	private double[][] simKnn(int vec, double[][] distance)
	{
		double[][] sim = new double[vec][vec];

		InfoComparator infoComparator = new InfoComparator();
		
		for(int i = 0; i < vec; i++)
		{
			Info[] infoList =  getInfo(distance, i);
			Arrays.sort(infoList, infoComparator);
			
			ArrayList<Info> nearestNeighbour = getNearestNeighbour(infoList);
			
			for(int j = 0; j < nearestNeighbour.size(); j++)
			{
				Info neighbour = nearestNeighbour.get(j);
				
				double coef = getSimilarityCoef(neighbour.dist);
				sim[neighbour.i][neighbour.j] = coef;
				sim[neighbour.j][neighbour.i] = coef;
			}			
		}
		return sim;
	}
	
	private double[][] simEps(int vec, double[][] distance)
	{
		double[][] sim = new double[vec][vec];
		
		for(int i = 0; i < vec; i++)
		{
			Info[] infoList = getInfo(distance, i);
			for(int j = 0; j < infoList.length; j++)
			{
				Info info = infoList[j];
				if(info.dist <= eps)
				{
					double coef = getSimilarityCoef(t * info.dist);
					sim[info.i][info.j] = coef;
					sim[info.j][info.i] = coef;
				}
			}			
		}
		return sim;
	}
	
	private Info[] getInfo(double[][] distance, int index)
	{
		Info[] infoList = new Info[distance[index].length - 1];
		
		for(int i = 0, counter = 0; i < distance[index].length; i++)
		{
			if(i != index)
			{
				infoList[counter] = new Info(index, i, distance[index][i]);
				counter++;
			}
		}
		
		return infoList;
	}
	
	private ArrayList<Info> getNearestNeighbour(Info[] info)
	{
		ArrayList<Info> infoList = new ArrayList<Info>();
		
		for(int i = 0; i < info.length && i < knn; i++)
		{
			infoList.add(info[i]);
		}
		if(knn < info.length)
		{
			Info tmp = info[knn -1];
			for(int i = knn; i < info.length; i++)
			{
				if(Math.abs(tmp.dist - info[i].dist) < 1e-8)
				{
					infoList.add(info[i]);
				}
			}
		}	
		return infoList;
	}
	
	private double[][] distance(int attr, int vec)
	{
		double[][] distance = new double[vec][vec];
		
		for(int i = 0; i < vec; i++)
		{
			double[] vec1 = dataModel.getVector(i);
			for(int j = 0; j < vec; j++)
			{
				if(i < j)
				{
					double[] vec2 = dataModel.getVector(j);
					double tmp = 0;
					for(int k = 0; k < attr; k++)
					{
						if(k != dataModel.getClassAttributeIndex())
						{
							tmp += Math.pow((vec1[k] - vec2[k]), 2);	
						}
					}
					distance[i][j] = Math.sqrt(tmp);
				} 
				else if(i == j)
				{
					distance[i][j] = 0;
				} 
				else
				{
					distance[i][j] = distance[j][i];
				}
			}
		}		
		return distance;
	}
	
	public int getKnn() {
		return knn;
	}

	public void setKnn(int knn) {
		this.knn = knn;
	}

	public double getT() {
		return t;
	}

	public void setT(double t) {
		this.t = t;
	}

	public double getEps() {
		return eps;
	}

	public void setEps(double eps) {
		this.eps = eps;
	}

	public String getOption() {
		return option;
	}

	public void setOption(String option) {
		this.option = option;
	}
}

class Info
{
	int j;
	int i;
	double dist;
	
	Info(int i, int j, double dist)
	{
		this.i = i;
		this.j = j;
		this.dist = dist;
	}
}

class InfoComparator implements Comparator<Info>
{
	public int compare(Info info1, Info info2)
	{
		if(info1.dist > info2.dist)
		{
			return 1;	
		}
		else if(info1.dist < info2.dist)
		{
			return -1;	
		}
		else
		{
			return 0;
		}
	}
}
